package panda.server;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Logger;

import panda.client.engine.CounterAndPos;
import panda.client.engine.MagicCard;
import panda.client.engine.MagicCardAndPos;
import panda.client.engine.MagicCardToken;
import panda.client.engine.ServerModel;
import panda.client.enumerations.ClientID;
import panda.client.enumerations.MagicSet;
import panda.client.services.DataService;
import panda.server.data.CardData;
import panda.server.data.MagicCardDB;
import panda.server.data.TokenData;
import panda.server.decklist.DeckEntry;
import panda.server.decklist.DeckScanner;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class DataServiceImpl extends RemoteServiceServlet implements DataService {
	private static final boolean DEBUG = true;
	private static final long serialVersionUID = 1L;
	private final Hashtable<ClientID, Boolean> clientStates = new Hashtable<ClientID, Boolean>();

	private List<CardData> serverCardDB = new ArrayList<CardData>();
	private Long serverID = 1L;

	private ServerModel model;
	private long startTimestamp = System.currentTimeMillis();
	private int clientsRegistered = 0;
	private ClientID lastClientWhoUpdated = null;
	private long modelNumber;
	public static final MagicSet baseSet = MagicSet.SCARS_OF_MIRRODIN;

	private static String nameOfLogger = DataServiceImpl.class.getName();
	private static Logger log = Logger.getLogger(nameOfLogger);

	public DataServiceImpl() {
		System.out.println("Loading at " + getTimestamp() + "ms");

		serverCardDB.addAll(MagicCardDB.getAllExtendedLegalCards());
		serverCardDB.addAll(MagicCardDB.getAllStandardLegalTokens());

		System.out.println("Done in: " + getTimestamp() + "ms");
	}

	private synchronized void initializeServerModel() {
		model = new ServerModel();
		model.setP1Life(20);
		model.setP2Life(20);
		model.setInPlay(new ArrayList<MagicCardAndPos>());
		model.setCounters(new ArrayList<CounterAndPos>());
		model.setP1gy(new ArrayList<MagicCard>());
		model.setP1hand(new ArrayList<MagicCard>());
		model.setP1lib(new ArrayList<MagicCard>());
		model.setP2gy(new ArrayList<MagicCard>());
		model.setP2hand(new ArrayList<MagicCard>());
		model.setP2lib(new ArrayList<MagicCard>());
		model.setUpdateMsg("Server has just started (or has been reset).");
		model.setModelNum(0);

		modelNumber = 0;

//		TimerTask notifier = new TimerTask() {
//			@Override
//			public void run() {
//				notifyAllCall();
//			}
//		};
//
//		Timer t = new Timer();
//
//		t.scheduleAtFixedRate(notifier, 7000, 5000);
	}

	public synchronized void notifyAllCall() {
		System.out.println("--- notifying all!");

		/* change all clientState values to Boolean.TRUE except the client that just updated */
		Set<ClientID> keys = clientStates.keySet();
		for(ClientID ID : keys) {
			clientStates.put(ID, Boolean.TRUE);
		}

		notifyAll();
	}

	/**
	 * Returns a new, unique serverID. Currently used for counter creation.
	 */
	public synchronized long getNewServerID() {
		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getNewServerID] is returning a new serverID: " + serverID);
		}

		return serverID++;
	}

	/**
	 * Generates a unique sessionID and Boolean hasChanged. Puts (sessionID, hasChanged) pair into server's HashTable.
	 * HasChanged is true because the client needs an initial update its model from the server.
	 */
	public synchronized ClientID registerClient(String placeholder /* will be used to let client pick his name */) {
		ClientID clientSessionID;

		/**
		 * Load decks and initialize server model when the first client registers. Not in constructor because
		 * getServletContext.getRealPath("/") throws an exception when called in the servlet's constructor
		 */
		if(clientsRegistered == 0) {
			initializeServerModel();
			loadDecks();
		}

		clientsRegistered++;
		switch(clientsRegistered) {
			case 1:
				clientSessionID = ClientID.P1;
				break;
			case 2:
				clientSessionID = ClientID.P2; // sessionIDs[0];
				break;
			case 3:
				clientSessionID = ClientID.OBSERVER; // sessionIDs[1];
				break;
			default:
				clientSessionID = ClientID.OBSERVER; // sessionIDs[2];
		}

		clientStates.put(clientSessionID, Boolean.TRUE);

		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:registerClient] " + clientSessionID + " registered");
		}

		return clientSessionID;
	}

	public synchronized ServerModel getModel(ClientID sessionID) {
		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getModel]    " + sessionID + " checking for updates");
		}

		while(clientStates.get(sessionID) == Boolean.FALSE) {
			try {
				if(DEBUG) {
					System.out.println(getTimestamp() + " [s:getModel]    ... and " + sessionID + " is waiting.");
				}

				wait();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}
		}

		clientStates.put(sessionID, Boolean.FALSE);

		if(DEBUG) {
			System.out.println(getTimestamp() + " [s:getModel]    model has changed for " + sessionID
					+ ".  Returning model to " + sessionID);
		}

		/**
		 * Code no longer exists: Gets only relevant calendars from the server. We truncate any calendars not being
		 * listened to by the client before sending back the (probably smaller) model.
		 */
		return model;
	}

	/**
	 * A client calls this method to update the server's data model. Changes all clientState values to Boolean.TRUE and
	 * notifies all waiting threads.
	 */
	public synchronized void updateModel(ClientID sessionID, ServerModel model) {
		/**
		 * prevents clients from different sessions from updating a freshly restarted server cardpanda session: ignores
		 * their requests
		 */
		if(model.getModelNum() != 0 && this.lastClientWhoUpdated == null) {
			log.info("client " + sessionID
					+ " is trying to update the server after it restarted recently. Ignoring request");
		} else {

			if(DEBUG) {
				// System.out.println(getTimestamp() + " [s:updateModel] model is being updated by [" + sessionID + " from "
				// + this.model.toString() + " to " + model.toString());
				log.info(sessionID + " is updating the server's model");
				System.out.println(getTimestamp() + " [s:updateModel] model is being updated by [" + sessionID + "]");
				System.out.println(getTimestamp() + " [s:updateModel] updater: [" + sessionID + "] (server#: "
						+ modelNumber + ", client#: " + model.getModelNum() + "). Last client who updated: "
						+ lastClientWhoUpdated);
				System.out.println(getTimestamp() + " [s:updateModel] update msg: " + model.getUpdateMsg());
			}

			/**
			 * Update the server copy of the model if: <br>
			 * 1. the model numbers match<br>
			 * 2. it's the same client updating as before<br>
			 * <br>
			 * There is no need to check for concurrent modification errors if it's the same client making many rapid
			 * changes, like incrementing life total or moving several cards around.
			 */
			if(model.getModelNum() == modelNumber || this.lastClientWhoUpdated.equals(sessionID)) {
				this.model = model;
				this.lastClientWhoUpdated = sessionID;

				/* the model has changed so we increment its model number */
				modelNumber++;
				this.model.setModelNum(modelNumber);

				/* change all clientState values to Boolean.TRUE except the client that just updated */
				Set<ClientID> keys = clientStates.keySet();
				for(ClientID ID : keys) {
					if(!sessionID.equals(ID)) {
						clientStates.put(ID, Boolean.TRUE);
					}
				}
				notifyAll();

				if(DEBUG) {
					System.out.println(getTimestamp() + " [s:updateModel] notifying all threads after model update from "
							+ sessionID);
				}
			} else { /* if the modelnumber doesn't match */

				/* update the model's updateMsg to let the user know his action failed */
				this.model.setUpdateMsg("Your opponent was moving something so your latest action was discarded =(");

				clientStates.put(sessionID, Boolean.TRUE);
				notifyAll();

				if(DEBUG) {
					System.out
							.println(getTimestamp()
									+ " [s:updateModel] notifying "
									+ sessionID.getName()
									+ " that its last action was undone due to concurrent modification.  Server received model number "
									+ model.getModelNum() + " but expected " + this.model.getModelNum());
				}
			}
		}
	}

	/**
	 * Grabs a card by set and id from the server - a linear operation wrt number of total cards in the DB. Also matches
	 * name, but name technically would not have been necessary to implement this function. As it is, it's required and
	 * an included argument mostly for ease of code reading. Throws NoSuchElementException if card isn't found. Cards are
	 * unique by set and id, so this stops when it finds the first match; there shouldn't be any other matches.
	 */
	public MagicCard getCardViaExactID(String name, MagicSet set, int id) {
		MagicCard result = null;
		for(CardData data : serverCardDB) {
			if(data.getName().toLowerCase().equals(name.toLowerCase()) && data.getMagicSet().equals(set)
					&& data.getID() == id) {

				/* add a -copy- of the card so setServerID doesn't set multiple copies of the same card to the same ID */
				/* also: make sure tokens remain tokens so we can differentiate them from normal MagicCards on the client */
				if(data instanceof TokenData) {
					result = new MagicCardToken(data.getName(), data.getMagicSet(), data.getID(), data.getCastingCost(),
							data.getType());
				} else {
					result = new MagicCard(data.getName(), data.getMagicSet(), data.getID(), data.getCastingCost(), data
							.getType());
				}

				synchronized(serverID) {
					result.setServerID(serverID++);
				}

				break;
			}
		}

		if(result == null) {
			throw new NoSuchElementException(name + " not found in collection");
		}

		return result;
	}

	/**
	 * Returns a List of all Standard Legal tokens as defined by MagicCardDB.java; used for TokenSummonDialog.
	 */
	public List<MagicCard> getAllTokensFromStandard() {
		System.out.println(getTimestamp() + " sending the set of all legal tokens in Standard to the client.");

		List<CardData> tokenData = MagicCardDB.getAllStandardLegalTokens();
		List<MagicCard> result = new ArrayList<MagicCard>();

		for(CardData data : tokenData) {
			result.add(new MagicCardToken(data.getName(), data.getMagicSet(), data.getID(), data.getCastingCost(), data
					.getType()));
		}

		return result;
	}

	public synchronized void terminateServer() {
		System.out.println("Exiting...");

		System.exit(0);
	}

	/* private methods */
	private long getTimestamp() {
		return System.currentTimeMillis() - startTimestamp + 10000000;
	}

	private void loadDecks() {
		String root = getServletContext().getRealPath("/");
		DeckScanner scanner = new DeckScanner(serverCardDB);
		System.out.println(root + "decks/" + "p1deck.txt");

		loadDeckToClientLibrary(ClientID.P1, scanner.assembleDecklistFromFile(root + "decks/" + "p1deck.txt"));
		loadDeckToClientLibrary(ClientID.P2, scanner.assembleDecklistFromFile(root + "decks/" + "p2deck.txt"));
	}

	/**
	 * Loads a decklist into a client's library.
	 * 
	 * @param ownername
	 * @param decklist
	 */
	private void loadDeckToClientLibrary(ClientID ownername, List<DeckEntry> decklist) {
		List<MagicCard> library;

		switch(ownername) {
			case P1:
				library = model.getP1lib();
				break;
			case P2:
				library = model.getP2lib();
				break;
			default:
				throw new IllegalArgumentException("Must be either P1 or P2");
		}

		for(DeckEntry entry : decklist) {
			MagicSet targetSet = null;
			/* this loop to match set abbreviation to a set is preeeeeety inefficient. */
			for(MagicSet set : MagicSet.values()) {
				if(set.getAbbrev().equals(entry.getMagicSetAbbrev())) {
					targetSet = set;
				}
			}

			assert targetSet != null : "Set " + entry.getMagicSetAbbrev() + " not found";

			/* add as many copies of the card as specified by the entry. each card has a unique serverID though */
			for(int i = 0; i < entry.getCopies(); i++) {
				MagicCard card = getCardViaExactID(entry.getCardName(), targetSet, entry.getID());
				card.setOwner(ownername);

				library.add(card);
			}
		}
	}
}